Skip to content

[8팀 현지수] Chapter 3-1. 프론트엔드 테스트 코드#63

Open
hyunzsu wants to merge 27 commits intohanghae-plus:hardfrom
hyunzsu:hard
Open

[8팀 현지수] Chapter 3-1. 프론트엔드 테스트 코드#63
hyunzsu wants to merge 27 commits intohanghae-plus:hardfrom
hyunzsu:hard

Conversation

@hyunzsu
Copy link

@hyunzsu hyunzsu commented Aug 21, 2025

HARD

7주차 과제 체크포인트

기본과제

  • 총 11개의 파일, 115개의 단위 테스트를 무사히 작성하고 통과시킨다.

질문

Q. handlersUtils에 남긴 질문에 답변해주세요.

// 문제 상황
const eventsData = [...events]; // 모든 테스트가 공유하는 전역 변수

// 테스트 A 실행
test('이벤트 생성', () => {
  eventsData.push(newEvent); // 전역 배열 수정
});

// 테스트 B 실행 (동시)
test('이벤트 목록 조회', () => {
  expect(eventsData).toHaveLength(2); // 예상: 2, 실제: 3 (테스트 A 영향)
});

문제 원인

MSW 핸들러 내부의 전역 변수가 문제의 원인이고, 테스트 간 데이터 상태가 공유되어 테스트 실행 순서에 따라 결과가 달라지는 동작이 발생했습니다.

독립적 데이터 환경 구축

// 해결: 각 테스트마다 독립적인 데이터 복사본 생성
export const setupMockHandlerCreation = (initEvents = []) => {
  const eventsData = [...initEvents]; // 매번 새로운 배열 생성
  
  return server.use(
    http.get('/events', () => Response.json(eventsData)),
    http.post('/events', async ({ request }) => {
      const newEvent = await request.json();
      eventsData.push(newEvent); // 이 테스트만의 독립적인 배열에 추가
      return Response.json(newEvent, { status: 201 });
    })
  );
};

목적별 핸들러 분리

// 생성 테스트용: GET + POST만
setupMockHandlerCreation(initEvents)

// 수정 테스트용: GET + PUT만  
setupMockHandlerUpdating(initEvents)

// 삭제 테스트용: GET + DELETE만
setupMockHandlerDeletion(initEvents)

단일 통합 함수 대신 테스트 목적별로 핸들러를 분리했습니다. server.use()로 런타임에 필요한 핸들러만 교체하여 메모리 사용량을 줄이고 테스트 의도를 명확하게 했습니다.

결과

// 병렬 테스트 안전성 검증
test('이벤트 생성', async () => {
  setupMockHandlerCreation([]); // 빈 배열로 시작
  const result = await createEvent(eventData);
  expect(result).toBeDefined(); // 다른 테스트 영향 없이 독립 실행
});

test('이벤트 삭제', async () => {
  setupMockHandlerDeletion([existingEvent]); // 독립적인 초기 데이터
  const result = await deleteEvent(existingEvent.id);
  expect(result.success).toBe(true);
});

각 테스트가 독립적인 데이터 환경을 가지게 되어 병렬 실행 시에도 안정적으로 동작합니다.
테스트 실행 순서에 관계없이 항상 동일한 결과를 보장합니다.

Q. 테스트를 독립적으로 구동시키기 위해 작성했던 설정들을 소개해주세요.

심화 과제

  • App 컴포넌트 적절한 단위의 컴포넌트, 훅, 유틸 함수로 분리했는가?
  • 해당 모듈들에 대한 적절한 테스트를 5개 이상 작성했는가?

과제 셀프회고

기술적 성장

커스텀 훅 테스팅 패턴

renderHook을 통한 훅 독립 테스트

컴포넌트 없이 훅만 독립적으로 실행하고 결과를 검증할 수 있는 개념

const { result } = renderHook(() => useProducts({ products, setProducts }));

act() 함수의 역할

상태 업데이트가 포함된 훅 테스트에서 React 렌더링 사이클과 테스트 환경을 동기화하는 요소

act(() => {
  result.current.addProduct(newProduct);
});

훅 테스트 3단계 패턴

  1. 초기 상태 검증: 훅이 올바른 초기값 반환
  2. 상태 변화 검증: 함수 호출 후 상태 변경 확인
  3. 사이드 이펙트 검증: 외부 의존성(알림, localStorage 등) 호출 확인

코드 품질

App 컴포넌트를 리팩토링했지만, 아직도 비대합니다.
props drilling을 최소화 할 수 있도록 리팩토링이 필요하다고 생각합니다.

학습 효과 분석

테스트 구조와 AAA 패턴

AAA 패턴

AAA(Arrange-Act-Assert) 패턴을 일관되게 적용했습니다. 각 단계가 명확히 구분되니 테스트를 읽기 쉬워지고 디버깅할 때도 어느 단계에서 문제가 생겼는지 바로 파악할 수 있었습니다.

const { result } = renderHook(() => useProducts({ products, setProducts }));

describe와 it을 통한 테스트 구조화

관련된 테스트들을 describe로 그룹화하면서 테스트가 체계적으로 구성되었습니다. 특히 중첩된 describe를 사용해 윤년 처리, 에러 케이스 등 로직별로 분류하니 테스트 의도가 훨씬 명확해졌습니다.

describe('getDaysInMonth', () => {
  describe('윤년 처리', () => {
    it('4로 나누어떨어지는 해는 윤년이다', () => {});
    it('100으로 나누어떨어지는 해는 평년이다', () => {});
    it('400으로 나누어떨어지는 해는 윤년이다', () => {});
  });
  
  describe('에러 케이스', () => {
    it('잘못된 월이 입력되면 에러를 던진다', () => {});
  });
});

Assertion 메서드 선택 기준

처음에는 toBe()만 썼다가 객체 비교에서 실패하는 경우를 겪었습니다. toBe는 원시값 비교, toEqual은 객체/배열 내용 비교라는 차이점을 이해하고 나서 상황에 맞는 assertion을 선택할 수 있게 되었습니다.

// 원시값 비교
expect(result).toBe(29);

// 객체/배열 내용 비교  
expect(dateObject).toEqual(expectedDate);

// 배열 포함 여부
expect(products).toContain(newProduct);

// 타입 검증
expect(result).toBeInstanceOf(Date);

리뷰 받고 싶은 내용

1. MSW 핸들러 설계의 적절성

현재 테스트 독립성을 위해 setupMockHandlerCreation, setupMockHandlerUpdating, setupMockHandlerDeletion으로 목적별 핸들러를 분리했습니다. 이런 목적별 분리 접근법이 MSW 모범 사례에 부합하는지 검토해주실 수 있나요? 대안으로 하나의 통합 핸들러에서 테스트 ID별로 데이터를 격리하는 방법도 고려해볼 만한지 의견을 듣고 싶습니다.

2. 하드코딩된 대기 시간의 적절성과 대안

// 비동기 처리 완료까지 대기
await new Promise((resolve) => setTimeout(resolve, 1000));

// 성공 확인 - 이벤트 리스트 또는 성공 메시지
await waitFor(() => {
  const hasEvent = within(eventList).queryByText('새로운 회의');
  const hasSuccessMessage = screen.queryByText('일정이 추가되었습니다.');
  expect(hasEvent || hasSuccessMessage).toBeTruthy();
});

API 호출 완료를 위해 1초 하드코딩 대기 후 waitFor로 재검증하는 패턴을 사용했습니다. 이런 이중 대기 방식이 테스트 신뢰성과 실행 시간 측면에서 효율적인지, 아니면 MSW 응답 완료를 더 정확히 감지할 수 있는 방법이 있는지 피드백 부탁드립니다.

hyunzsu added 27 commits August 19, 2025 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant